﻿using System;
using System.Windows.Input;
using System.ComponentModel;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Linq.Expressions;
using System.Diagnostics;

namespace LAeMail.Infrastructure
{
    /// <summary>
    /// Property Helpers base class.
    /// </summary>
    public abstract class PropertyHelpersBase : INotifyPropertyChanged, IDisposable
    {
        #region Private
        private readonly Dictionary<string, object> values = new Dictionary<string, object>();
        private readonly List<KeyValuePair<string, string>> derived = new List<KeyValuePair<string, string>>();
        #endregion // Private

        #region Constructor
        protected PropertyHelpersBase()
        {
        }
        #endregion // Constructor

        #region INotifyPropertyChanged Members

        /// <summary>
        /// Raised when a property on this object has a new value.
        /// </summary>
        public event PropertyChangedEventHandler PropertyChanged;

        /// <summary>
        /// Raises this object's PropertyChanged event.
        /// </summary>
        /// <param name="propertyName">The property that has a new value.</param>
        protected virtual void OnPropertyChanged(string propertyName)
        {
            this.VerifyPropertyName(propertyName);

            PropertyChangedEventHandler handler = this.PropertyChanged;
            if (handler != null)
            {
                var e = new PropertyChangedEventArgs(propertyName);
                handler(this, e);
            }
        }

        #endregion // INotifyPropertyChanged Members

        #region Debugging Aides

        /// <summary>
        /// Warns the developer if this object does not have
        /// a public property with the specified name. This 
        /// method does not exist in a Release build.
        /// </summary>
        [Conditional("DEBUG")]
        [DebuggerStepThrough]
        public void VerifyPropertyName(string propertyName)
        {
            // Verify that the property name matches a real,  
            // public, instance property on this object.
            if (TypeDescriptor.GetProperties(this)[propertyName] == null)
            {
                string msg = "Invalid property name: " + propertyName;

                if (this.ThrowOnInvalidPropertyName)
                    throw new Exception(msg);
                else
                    Debug.Fail(msg);
            }
        }

        /// <summary>
        /// Returns whether an exception is thrown, or if a Debug.Fail() is used
        /// when an invalid property name is passed to the VerifyPropertyName method.
        /// The default value is false, but subclasses used by unit tests might 
        /// override this property's getter to return true.
        /// </summary>
        protected virtual bool ThrowOnInvalidPropertyName { get; private set; }

        #endregion // Debugging Aides

        #region IDisposable Members

        /// <summary>
        /// Invoked when this object is being removed from the application
        /// and will be subject to garbage collection.
        /// </summary>
        public void Dispose()
        {
            this.OnDispose();
        }

        /// <summary>
        /// Child classes can override this method to perform 
        /// clean-up logic, such as removing event handlers.
        /// </summary>
        protected virtual void OnDispose()
        {
        }

#if DEBUG
        /// <summary>
        /// Useful for ensuring that ViewModel objects are properly garbage collected.
        /// </summary>
        ~PropertyHelpersBase()
        {
            string msg = string.Format("{0} ({1}) Finalized", this.GetType().Name, this.GetHashCode());
            System.Diagnostics.Debug.WriteLine(msg);
        }
#endif

        #endregion // IDisposable Members
        #region Property Helpers
        protected string PropertyName<T>(Expression<Func<T>> expression)
        {
            var memberExpression = expression.Body as MemberExpression;

            if (memberExpression == null)
                throw new ArgumentException("expression must be a property expression");

            return memberExpression.Member.Name;
        }

        protected void AddDerivedProperty(string parentName, string derivedName)
        {
            if (this.GetType().GetProperty(derivedName) != null) //only add derived properties for properties that are recognized
                derived.Add(new KeyValuePair<string, string>(parentName, derivedName));
        }

        private void RaiseDerivedPropertyChanges(string parentName)
        {
            if (derived.Count == 0)
                return;

            var results = derived.Where(s => s.Key == parentName);

            foreach (var kv in results)
                this.OnPropertyChanged(kv.Value);
        }

        #endregion

        #region Getters
        protected T Get<T>(string name)
        {
            return Get(name, default(T));
        }

        protected T Get<T>(string name, T defaultValue)
        {
            if (values.ContainsKey(name))
            {
                return (T)values[name];
            }

            return defaultValue;
        }

        protected T Get<T>(Expression<Func<T>> expression)
        {
            return Get<T>(PropertyName(expression));
        }

        protected T Get<T>(Expression<Func<T>> expression, T defaultValue)
        {
            return Get(PropertyName(expression), defaultValue);
        }

        protected T Get<T>(Expression<Func<T>> expression, Func<T> initialValue)
        {
            return Get(PropertyName(expression), initialValue);
        }

        protected T Get<T>(string name, Func<T> initialValue)
        {
            if (values.ContainsKey(name))
            {
                return (T)values[name];
            }

            Set(name, initialValue());
            return Get<T>(name);
        }
        #endregion

        #region Setters
        protected void Set<T>(Expression<Func<T>> expression, T value)
        {
            Set(PropertyName(expression), value);
        }

        protected void Set<T>(string name, T value)
        {
            if (values.ContainsKey(name))
            {
                if (values[name] == null && value == null)
                    return;

                if (values[name] != null && values[name].Equals(value))
                    return;

                values[name] = value;
            }
            else
            {
                values.Add(name, value);
            }
            this.OnPropertyChanged(name);
            RaiseDerivedPropertyChanges(name);
        }

        public void Set(String key, String value)
        {
            PropertyInfo pi = this.GetType().GetProperty(key);

            if (pi != null) //only process keys that are recognized
                Set(key, Convert.ChangeType(value, pi.PropertyType));
        }
        #endregion
    }
}